/**
 * 
 */
package com.vision.core.sv.serv.rpc;

import static com.vision.core.cm.data.AdminPermissions.CreateUser;
import static com.vision.core.cm.data.AdminPermissions.DeleteUser;
import static com.vision.core.cm.data.AdminPermissions.UpdateOwnProfile;
import static com.vision.core.cm.data.AdminPermissions.UpdateUser;
import static com.vision.core.cm.data.AdminPermissions.ViewUser;

import java.sql.Timestamp;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;

import org.hibernate.criterion.Restrictions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.vision.core.cl.CoreValidators;
import com.vision.core.cm.data.Domain;
import com.vision.core.cm.db.data.Company;
import com.vision.core.cm.db.data.Role;
import com.vision.core.cm.db.data.User;
import com.vision.core.cm.db.data.User.UserStatus;
import com.vision.core.cm.exception.DataNotFoundException;
import com.vision.core.cm.exception.RpcException;
import com.vision.core.cm.serv.rpc.UserService;
import com.vision.core.sv.db.mgr.IDataManager;
import com.vision.core.sv.db.mgr.ISimpleCriteria;
import com.vision.core.sv.db.mgr.SimpleCriteria;
import com.vision.core.sv.security.Encrypt;
import com.vision.core.sv.security.SessionUtils;

/**
 * TODO When returning User instances back to client, strip out the password.
 * Note though that the stripping out must be done outside Spring's transaction context.
 * 
 * @author Mark
 *
 */
@Service("users")
public class UserServiceImpl extends AbstractServiceImpl<User> implements UserService {

	private static final long serialVersionUID = 1L;
	
	private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
	
	public UserServiceImpl() {
		initGilead();
	}

	@Autowired
	@Qualifier("userManager")
	@Override
	public void setDataManager(IDataManager<User> dataManager) {
		super.setDataManager(dataManager);
	}
	
	// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	@Override
	public User getProfile() throws RpcException {
		checkPermissions(UpdateOwnProfile);
		
		String username = getLoggedInUser().getUsername();
		User user = getDataManager().getById(username);
		user.setPassword(null);
		
		return user;
	}
	
	@Override
	public User getByUsername(String username) throws RpcException {
		checkPermissions(ViewUser);
		
		User user = getDataManager().getById(username);
		user.setPassword(null);
		
		return user;
	}

	@Transactional
	@Override
	public User create(User user) throws RpcException {
		checkPermissions(CreateUser);
		checkUsername(user.getUsername());
		
		String cleanedUsername = user.getUsername().toLowerCase().trim();
		user.setUsername(cleanedUsername);
		
		checkNonExisting(user);
		
		user.setPassword(Encrypt.encode(user.getPassword()));
		user.setDateCreated(new Timestamp(System.currentTimeMillis()));
		user.setStatus(UserStatus.ACTIVE);
		
		log.debug("Creating user: {}", user.getUsername());

		User result = super.create(user);
		
		logCreate(result);
		
		return result;
	}
	
	@Override
	public List<User> getAll() throws RpcException {
		checkPermissions(ViewUser);
		
		List<User> users = super.getAll();
		
		User loggedIn = getLoggedInUser();
		
		for (int i = users.size() - 1; i >= 0; i--) {
			User user = users.get(i);
			// Remove the root user from the list if the currently logged in user is not root
			if (User.ROOT_USERNAME.equals(user.getUsername()) && !User.ROOT_USERNAME.equals(loggedIn.getUsername()))
				users.remove(i);
			
			user.setPassword(null); // Strip out the password
		}
		
		Collections.sort(users, new Comparator<User>() {
			@Override
			public int compare(User arg0, User arg1) {
				return arg0.getUsername().compareTo(arg1.getUsername());
			}
		});
				
		return users;
	}
	
	@Override
	public User getById(long id) throws RpcException {
		throw new RpcException("This operation is not supported.");
	}

	/**
	 * NOTE This method should be invoked only from the Administrator module where
	 * the administrator is allowed to override a {@link User}'s password without
	 * the need to provide the {@link User}'s current password.
	 */
	@Transactional
	@Override
	public User update(User user) throws RpcException {
		checkPermissions(UpdateUser);
		User existing = checkExisting(user);
		
		// If a new password is specified, update the password
		String newPassword = user.getPassword();
		if (newPassword != null && !newPassword.isEmpty()) {
			existing.setPassword(Encrypt.encode(newPassword));
		}
		
		existing.setEmail(user.getEmail());
		
		existing.setFirstName(user.getFirstName());
		existing.setMiddleName(user.getMiddleName());
		existing.setLastName(user.getLastName());
		
		log.debug("Updating user: {}", existing.getUsername());
		
		User result = super.update(existing);
		
		logUpdate(existing, result);
		
		return result;
	}
	
	@Transactional
	@Override
	public User delete(User object) throws RpcException {
		checkPermissions(DeleteUser);
		
		User existing = checkExisting(object);
		if (User.ROOT_USERNAME.equals(existing.getUsername()))
			throw new RpcException("User 'root' cannot be deleted.", null);
		
		// Deleting user's own account
		if (existing.equals( SessionUtils.getUser() ))
			throw new RpcException("You cannot delete your own user account.", null);
		
		log.debug("Deleting user: {}", existing.getUsername());
		
		User result = super.delete(existing);
		
		logDelete(result);
		
		return result;
	}
	
	@Transactional
	@Override
	public User updateOwnProfile(User user, String oldPassword) throws RpcException {
		checkPermissions(UpdateOwnProfile);
		
		log.debug("User requests to update his/her profile information.");
		
		User loggedIn = getLoggedInUser();
		if (!loggedIn.getUsername().equals(user.getUsername()))
			throw new RpcException("You are not the currently logged-in User.", null);
		
		// Check if the old password provided is the same as the current password
		String newPassword = user.getPassword();
		if (newPassword != null) {
			log.debug("User specified a new password. Checking with old password...");
			String encOldPassword = Encrypt.encode(oldPassword);
			if (!loggedIn.getPassword().equals(encOldPassword))
				throw new RpcException("The password you provided does not match the current password.");
			
			user.setPassword(Encrypt.encode(newPassword));
		} else {
			user.setPassword(loggedIn.getPassword());
		}
		
		// Ignore Status
		user.setStatus(loggedIn.getStatus());
		
		// Ignore Companies and Roles
		user.setCompanies(new HashSet<Company>(loggedIn.getCompanies()));
		user.setRoles(new HashSet<Role>(loggedIn.getRoles()));
		
		log.debug("Updating own profile: {}", user.getUsername());
		
		User result = super.update(user);
		
		logUpdate(loggedIn, result);
		
		return result;
	}
	
	@Transactional
	@Override
	public List<Company> assignCompanies(User user, List<Company> companies) throws RpcException {
		checkPermissions(UpdateUser);
		
		User existing = checkExisting(user);
		
		log.debug("Assigning {} companies to user: {}", companies.size(), existing.getUsername());
		for (Company company : companies)
			existing.addCompany(company);
				
		super.update(existing);
				
		logAssociate(existing, companies);
		
		return companies;
	}

	@Transactional
	@Override
	public List<Company> unassignCompanies(User user, List<Company> companies) throws RpcException {
		checkPermissions(UpdateUser);
		
		User existing = checkExisting(user);
		if (User.ROOT_USERNAME.equals(existing.getUsername()))
			throw new RpcException("Cannot unassign companies from this user.", null);
		
		log.debug("Unassigning {} companies from user: {}", companies.size(), existing.getUsername());
		for (Company company : companies)
			existing.removeCompany(company);
		
		super.update(existing);
				
		logDeassociate(existing, companies);
		
		return companies;
	}

	@Transactional
	@Override
	public List<Role> assignRoles(User user, List<Role> roles) throws RpcException {
		checkPermissions(UpdateUser);
		
		User existing = checkExisting(user);
		
		log.debug("Assigning roles to user: {}", existing.getUsername());
		for (Role role : roles)
			existing.addRole(role);
				
		super.update(existing);
				
		logAssociate(existing, roles);
		
		return roles;
	}

	@Transactional
	@Override
	public List<Role> unassignRoles(User user, List<Role> roles) throws RpcException {
		checkPermissions(UpdateUser);
		
		User existing = checkExisting(user);
		if (User.ROOT_USERNAME.equals(existing.getUsername()))
			throw new RpcException("Cannot unassign roles from this user.", null);

		log.debug("Unassigning roles from user: {}", existing.getUsername());
		for (Role role : roles)
			existing.removeRole(role);
				
		super.update(existing);
				
		logDeassociate(existing, roles);
		
		return roles;
	}
	
	@Transactional
	@Override
	public User activate(User user) throws RpcException {
		checkPermissions(UpdateUser);
		
		User existing = checkExisting(user);
		existing.setStatus(UserStatus.ACTIVE);
		
		log.debug("Activating user: {}", existing.getUsername());
		
		User result = super.update(existing);
		
		logUpdate(existing, result);
		
		return result;
	}

	@Transactional
	@Override
	public User deactivate(User user) throws RpcException {
		checkPermissions(UpdateUser);
		
		User existing = checkExisting(user);
		if (User.ROOT_USERNAME.equals(existing.getUsername()))
			throw new RpcException("User cannnot be deactivated.", null);
		
		// Deactivating user's own account
		if (existing.equals( SessionUtils.getUser() ))
			throw new RpcException("You cannot deactivate your own user account.", null);
		
		existing.setStatus(UserStatus.DEACTIVATED);
		
		log.debug("Deactivating user: {}", existing.getUsername());
		
		User result = super.update(existing);
		
		logUpdate(existing, result);
		
		return result;
	}
	
	@Override
	protected Domain getDomain() {
		return Domain.USER;
	}

	// Helpers *****************************************************************

	protected void checkNonExisting(User param) throws RpcException {
		ISimpleCriteria c = SimpleCriteria.forClass(User.class);
		c.or(Restrictions.ilike("username", param.getUsername()), Restrictions.ilike("email", param.getEmail()));
		List<User> users = findByCriteria(c);
		
		if (!users.isEmpty()) {
			throw new RpcException("The username '" + param.getUsername() + "' or the email address '" + param.getEmail() + "' is already in use.");
		}
	}
	
	protected User checkExisting(User param) throws DataNotFoundException {
		User existingUser = getDataManager().getById(param.getUsername());
		if (existingUser == null)
			throw new DataNotFoundException("The User does not exist.");
		return existingUser;
	}
	
	private static void checkUsername(String username) throws RpcException {
		String v = username == null ? "" : username.trim();
		if (v.isEmpty())
			throw new RpcException("Username must not be empty.");
		
		if (!v.matches(CoreValidators.USERNAME_REGEX))
			throw new RpcException("The username does not follow a valid format.");
	}
	
}
